<!DOCTYPE HTML>
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <meta name="google-site-verification" content="KEatQX-J4dYY-6J2KU_aP5X8gAJ8wS0lhylI8umX6WA" />
    <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
    <link rel="shortcut icon" href="../images/favicon.ico">
    <link rel="stylesheet" href="../css/code.css" type="text/css"/>
    <link rel="stylesheet" href="../css/bootstrap.css" type="text/css"/>
    <link rel="stylesheet" href="../css/main.css" type="text/css"/>
    <title>编程小梦|Kylin 2.0 Spark Cubing 优化改进</title>
</head>
<body>
<nav class="navbar navbar-default navbar-static-top" style="opacity: .9" role="navigation">
    <div class="container-fluid">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse"
                    data-target="#bs-example-navbar-collapse-1">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">编程小梦</a>
        </div>
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav navbar-right">
                <li class="active"><a href="/">Blog</a></li>
                
                <li><a href="https://github.com/kangkaisen" target="_blank" rel="nofollow">GitHub</a></li>
                
                
                <li><a href="http://weibo.com/533234148" target="_blank" rel="nofollow">WeiBo</a></li>
                
            </ul>
        </div>
    </div>
</nav>
<div class="row" style="padding-top: 60px">
    <div class="container center-block">
        <div class="col-md-1"></div>
        <div class="col-md-10 col-sm-12">
            <h1> Kylin 2.0 Spark Cubing 优化改进</h1>
            <hr/>
            <p>作者: 康凯森</p>
            <p>日期: 2017-07-02</p>
            <p>分类: <a href="../tag/OLAP.html" target="_blank" >OLAP</a></p>
            <hr/>
            <!-- toc -->
<ul>
<li><a href="#spark-cubing-简介">Spark Cubing 简介</a></li>
<li><a href="#spark-cubing-访问-kerberos认证的-hbase-解1">Spark Cubing 访问 Kerberos认证的 HBase 解1</a></li>
<li><a href="#spark-cubing-访问-kerberos认证的-hbase-解2">Spark Cubing 访问 Kerberos认证的 HBase 解2</a></li>
<li><a href="#spark-cubing-参数配置">Spark Cubing 参数配置</a></li>
<li><a href="#spark-cubing-的构建性能">Spark Cubing 的构建性能</a></li>
<li><a href="#spark-cubing-的资源消耗">Spark Cubing 的资源消耗</a></li>
<li><a href="#spark-cubing-的优缺点">Spark Cubing 的优缺点</a></li>
<li><a href="#spark-cubing-的适用场景">Spark Cubing 的适用场景</a></li>
<li><a href="#spark-cubing-字典加载优化">Spark Cubing 字典加载优化</a></li>
<li><a href="#spark-学习资料推荐">Spark 学习资料推荐</a></li>
<li><a href="#总结">总结</a></li>
</ul>
<!-- toc stop -->
<p>Kylin 2.0 引入了Spark Cubing beta版本，本文主要介绍我是如何让 Spark Cubing 支持 启用Kerberos的HBase集群，再介绍下Spark Cubing的性能测试结果和适用场景。</p>
<h3 id="spark-cubing-简介">Spark Cubing 简介</h3>
<p>在简介Spark Cubing之前，我简介下MapReduce Batch Cubing。所谓的MapReduce Batch Cubing就是利用MapReduce 计算引擎 批量计算Cube，其输入是Hive表，输出是HBase的KeyValue，整个构建过程主要包含以下6步：</p>
<ol>
<li>建立Hive的大宽表； <strong>（MapReduce计算）</strong></li>
<li>对需要字典编码的列计算列基数； <strong>（MapReduce计算）</strong></li>
<li>构建字典； <strong>（JobServer计算 or MapReduce计算）</strong></li>
<li>分层构建Cuboid； <strong>（MapReduce计算）</strong></li>
<li>将Cuboid转为HBase的KeyValue结构（HFile）； <strong>（MapReduce计算）</strong></li>
<li>元数据更新和垃圾回收。</li>
</ol>
<p>详细的Cube生成过程可以参考 <a href="http://blog.bcmeng.com/post/kylin-cube.html">Apache Kylin Cube 构建原理</a>。</p>
<p><strong>而Kylin 2.0的Spark Cubing就是在Cube构建的第4步替换掉MapReduce。</strong></p>
<p>如下图，就是将5个MR job转换为1个Spark job：</p>
<p>（注：<code>以下两个图片引自 Apache Kylin 官网的blog</code>： <a href="http://kylin.apache.org/blog/2017/02/23/by-layer-spark-cubing/">By-layer Spark Cubing</a>， 更详细的介绍也可以参考这篇blog。）</p>
<p>MapReduce 计算5层的Cuboid会用5个MR Application计算：
<img src="http://kylin.apache.org/images/blog/spark-mr-layer.png" alt="此处输入图片的描述"></p>
<p>Spark 计算Cuboid只会用1个 Application计算：
<img src="http://kylin.apache.org/images/blog/spark-cubing-layer.png" alt="此处输入图片的描述"></p>
<p>Spark Cubing的核心实现类是<code>SparkCubingByLayer</code>。</p>
<h4 id="为什么目前只有计算cuboid这一步用spark计算？">为什么目前只有计算Cuboid这一步用Spark计算？</h4>
<p>个人认为主要有以下两点：</p>
<ol>
<li>实现并不复杂。</li>
<li>收益会比较明显。Cuboid的分层计算方法天然可以利用RDD的Cache特性来加速Cuboid 计算，最理想的情况，如果executor的内存能够完全cache住每层Cuboid的RDD，那就可以完全避免读写磁盘，必然会比MapReduce快很多。</li>
</ol>
<p>我认为第2点是主要原因。</p>
<p><strong>Cube构建的其他步骤不可以用Spark计算吗？</strong></p>
<p>当然可以！ 其中第1步 建立Hive的大宽表 和 第5步 生成HFile 替换为Spark是十分简单的，但是<strong>性能提升可能不会十分明显</strong>。 至于2步计算列基数，<strong>其代码逻辑应该是整个Cube构建中最复杂的一步</strong>，复杂的主要原因就是这一步肩负的使命略多。 还有第3步MR构建字典，<strong>因为MR构建本身尚不成熟，自然不急着迁移到Spark</strong>。</p>
<p><strong>Spark Cubing beta版本目前最大的问题就是不支持启用Kerberos认证的HBase集群</strong>，而事实上不少企业级的HBase服务都启用了Kerberos认证。不支持的原因主要是Spark Cubing需要直接从HBase中访问cube，dict等元数据信息。</p>
<h3 id="spark-cubing-访问-kerberos认证的-hbase-解1">Spark Cubing 访问 Kerberos认证的 HBase 解1</h3>
<p>第一种简单的做法是将访问HBase的token从Kylin的JobServer传递到executor中，这种做法的限制是只能运行在Yarn-client模式中，即必须让driver运行在Kylin的JobServer中。 关于yarn-cluster mode和yarn-client mode两种模式的区别可以参考：
<a href="http://blog.cloudera.com/blog/2014/05/apache-spark-resource-management-and-yarn-app-models/">Apache Spark Resource Management and YARN App Models</a>。</p>
<p>这种做法的实现方式很简单，只需在SparkCubingByLayer的new SparkConf()之前加入以下3行代码：</p>
<pre><code>        Configuration configuration = HBaseConnection.getCurrentHBaseConfiguration();
        HConnection connection = HConnectionManager.createConnection(configuration);
        TokenUtil.obtainAndCacheToken(connection, UserProvider.instantiate(configuration).create(UserGroupInformation.getCurrentUser()));
</code></pre><p>但是如果只能在yarn-client模式下运行，必然无法运行在生产环境，因为Kylin JobServer机器的内存肯定不够用。</p>
<h3 id="spark-cubing-访问-kerberos认证的-hbase-解2">Spark Cubing 访问 Kerberos认证的 HBase 解2</h3>
<p>既然Spark Cubing在 启用Kerberos认证的 HBase集群下无法运行的根本原因是 Spark Cubing需要从HBase 直接访问Job相关的Kylin元数据， 那我们把元数据换个地方存不就可以了， 所以<strong>我们将每个Spark Job相关的Kylin元数据上传到HDFS，并用Kylin的HDFSResourceStore来管理元数据</strong>。</p>
<p>在介绍实现思路前，我先简介下Kylin元数据的存储结构和Kylin的ResourceStore。</p>
<p>首先，<strong>Kylin每个具体的元数据都是一个JSON文件，整个元数据的组织结构是个树状的文件目录。</strong>  如图是Kylin元数据的根目录：
<img src="http://static.zybuluo.com/kangkaisen/t1tc6neiaebiyfoir4fdhs11/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-07-02%20%E4%B8%8B%E5%8D%883.51.43.png" alt="屏幕快照 2017-07-02 下午3.51.43.png-20.7kB">
下图是project目录下的具体内容，其中learn_kylin和kylin_test都是project名称：
<img src="http://static.zybuluo.com/kangkaisen/4dtiioqnw08w6vtj0r9u5f27/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-07-02%20%E4%B8%8B%E5%8D%883.54.59.png" alt="屏幕快照 2017-07-02 下午3.54.59.png-11.8kB"></p>
<p>我们知道Kylin元数据的组织结构后，再简介下Kylin元数据的存储方式。 元数据存储的抽象类是ResourceStore，具体的实现类共有3个：</p>
<ul>
<li>FileResourceStore  本地文件系统  </li>
<li>HBaseResourceStore  HBase          </li>
<li>HDFSResourceStore HDFS           </li>
</ul>
<p>其中<strong>只有HBase可以用于生产环境，本地文件系统主要用来测试，HDFS不能用于生产的原因是并发处理方面还有些问题</strong>。具体用哪个ResourceStore是通过配置文件的kylin.metadata.url来决定的。</p>
<p><strong>所以下面的问题就是我们如何将HBase中的元数据转移到HDFS 和如何将HBaseResourceStore 转为 HDFSResourceStore？</strong></p>
<ol>
<li>确定Spark Job需要读取哪些Kylin元数据</li>
<li>将需要的Kylin元数据dump到本地</li>
<li>改写kylin.metadata.url并将所有配置写到本地的元数据目录</li>
<li>利用ResourceTool将本地的元数据上传到指定的HDFS目录</li>
<li>在Spark executor中根据指定HDFS元数据目录的Kylin配置文件构造出HDFSResourceStore。</li>
</ol>
<p>当然，在最后我们需要清理掉指定HDFS目录的元数据。 整个思路比较简单清晰，但是实际实现中还是有很多细节需要去考虑。</p>
<h3 id="spark-cubing-参数配置">Spark Cubing 参数配置</h3>
<p>以下是我使用的Spark配置，目的是尽可能让用户不需要关心Spark的配置</p>
<pre><code>//运行在yarn-cluster模式
kylin.engine.spark-conf.spark.master=yarn
kylin.engine.spark-conf.spark.submit.deployMode=cluster 

//启动动态资源分配，个人认为在Kylin生产场景中是必须的，因为我们不可能让每个用户自己去指定executor的个数
kylin.engine.spark-conf.spark.dynamicAllocation.enabled=true
kylin.engine.spark-conf.spark.dynamicAllocation.minExecutors=10
kylin.engine.spark-conf.spark.dynamicAllocation.maxExecutors=1024
kylin.engine.spark-conf.spark.dynamicAllocation.executorIdleTimeout=300
kylin.engine.spark-conf.spark.shuffle.service.enabled=true
kylin.engine.spark-conf.spark.shuffle.service.port=7337

//内存设置
kylin.engine.spark-conf.spark.driver.memory=4G
//数据规模较大或者字典较大时可以调大executor内存
kylin.engine.spark-conf.spark.executor.memory=4G 
kylin.engine.spark-conf.spark.executor.cores=1

//心跳超时
kylin.engine.spark-conf.spark.network.timeout=600

//队列设置
kylin.engine.spark-conf.spark.yarn.queue=root.rz.hadoop-hdp.test

//分区大小
kylin.engine.spark.rdd-partition-cut-mb=100
</code></pre><h3 id="spark-cubing-的构建性能">Spark Cubing 的构建性能</h3>
<p>对于百万级，千万级，亿级的源数据，且无很大字典的情况下，我的测试结果和官方<a href="http://kylin.apache.org/blog/2017/02/23/by-layer-spark-cubing/">By-layer Spark Cubing</a> 的结果基本一致，构建速度提升比较明显，而且Cuboid的层次数越多，提速越明显。</p>
<p>此外，我专门测试了数十亿级源数据或者有超大字典的情况，构建提速也十分明显：</p>
<h4 id="测试cube1">测试Cube1</h4>
<p>原始数据量： 27亿行  9个维度  包含1个精确去重指标 字典基数7千多万</p>
<p>MR Cuboid构建耗时：  75分钟</p>
<p>Spark Cuboid第一次构建耗时： 40分钟
（spark.executor.memory = 8G，没有加spark.memory.fraction参数）</p>
<p>Spark Cuboid第二次构建耗时： 24分钟
（spark.executor.memory = 8G，spark.memory.fraction = 0.5）</p>
<p>为什么减小spark.memory.fraction可以加速构建？</p>
<p>因为减小spark.memory.fraction，可以增大executor中User Memory的大小，给Kylin字典更多的内存，这样就可以避免全局字典换入换出，减少GC。</p>
<h4 id="测试cube2">测试Cube2</h4>
<p>原始数据量：24亿行  13个维度 38个指标（其中9个精确去重指标） 不过这个cube的精确去重指标基数比较小，只有几百万。</p>
<p>MR Cuboid构建耗时： 31分钟</p>
<p>Spark Cuboid构建耗时： 8分钟</p>
<p>总结来说，<code>Spark Cubing的构建性能相比MR有1倍到3倍的提升</code>。</p>
<h3 id="spark-cubing-的资源消耗">Spark Cubing 的资源消耗</h3>
<p>除了构建性能，我们肯定还会关注资源消耗。在这次测试中我没有对所以测试结果进行资源消耗分析，只分析了几个Cube。</p>
<p>我的结论是，<code>在我采用的Spark配置情况下，对于中小规模数据集Spark的资源消耗是小于MR的</code>（executor的内存是4G）； 对于有大字典的情况（executor的内存是8G），CPU资源Spark是小于MR的，但是内存资源Spark会比MR略多，<code>在这种情况下，我们相当于用内存资源来换取了执行效率</code>。</p>
<h3 id="spark-cubing-的优缺点">Spark Cubing 的优缺点</h3>
<p>优点：</p>
<ul>
<li>利用RDD的Cache特性，尽可能利用内存来避免重复IO</li>
<li>大部分场景下Cuboid构建速度有明显提升</li>
<li>在集群资源充足的情况下，我们可以用更多的资源换取更好的构建性能</li>
</ul>
<p>缺点：</p>
<ul>
<li>目前版本还未历经生产环境考验，稳定性不确定</li>
<li>不适合有超大字典的场景</li>
<li>引入Spark Cubing将带来额外的运维成本和沟通成本</li>
</ul>
<h3 id="spark-cubing-的适用场景">Spark Cubing 的适用场景</h3>
<p>个人的结论是，<code>除了有好几亿基数超大字典的这种情况，其他情况应该都适用Spark Cubing</code>，其中：</p>
<ol>
<li>Cuboid层次越多越适用。</li>
<li>数据规模越小越适用。</li>
<li>字典越小越适用。</li>
</ol>
<h3 id="spark-cubing-字典加载优化">Spark Cubing 字典加载优化</h3>
<p>Spark和MR有一点重要的区别就是<strong>Spark的Task是在线程中执行的，MR的Task是在进程中执行的</strong>。 这点区别会对Kylin的Cube 构建造成重要影响，在MR Cubing中，每个Mapper task 只需要load一次字典，但是在Spark Cubing中，一个executor的多个task会多次load 字典，如果字典较大，就会造成频繁GC，导致执行变慢。</p>
<p>针对这个问题，我做了两点优化：</p>
<ol>
<li>让每个executor里的字典只load一次，让该executor的所有Task共享字典。</li>
<li>给全局字典的AppendTrieDictionary中使用的LoadingCache增加maximumSize。 <strong>我用了一个有6亿基数的全局字典测试了这个优化，优化后GC时间明显缩短。</strong></li>
</ol>
<h3 id="spark-学习资料推荐">Spark 学习资料推荐</h3>
<p>网上公开资料如果只推荐一份的话，我推荐：
<a href="https://spark-internals.books.yourtion.com/">spark-internals</a></p>
<p>此外，这几篇文章也不错：</p>
<p><a href="https://0x0fff.com/spark-memory-management/">Spark Memory Management</a></p>
<p><a href="https://0x0fff.com/spark-architecture-shuffle/">Spark Architecture: Shuffle</a></p>
<p><a href="http://blog.cloudera.com/blog/2015/03/how-to-tune-your-apache-spark-jobs-part-1/">how-to-tune-your-apache-spark-jobs-part-1</a></p>
<p><a href="http://blog.cloudera.com/blog/2015/03/how-to-tune-your-apache-spark-jobs-part-2/">how-to-tune-your-apache-spark-jobs-part-2</a></p>
<p><a href="http://tech.meituan.com/spark-tuning-basic.html">Spark性能优化指南——基础篇</a></p>
<p><a href="http://tech.meituan.com/spark-tuning-pro.html">Spark性能优化指南——高级篇</a></p>
<p><a href="http://jerryshao.me/architecture/2013/04/21/Spark%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8B-scheduler%E6%A8%A1%E5%9D%97/">Spark源码分析之-scheduler模块</a></p>
<p>当然，看的资料再多自己不思考都没啥卵用。 学习一个系统，我们可以从系统的整体架构和设计层面开始，自顶向下的学习，也可以从一个具体的问题把整个系统涉及的所有模块串起来，切面式学习。 个人感觉两种方式结合着效率会比较高，而且一般从具体问题入手会让印象更深刻，理解更深入。</p>
<h3 id="总结">总结</h3>
<p>用HDFSResourceStore替换HBaseResourceStore后，Spark Cubing已经具备了在启用Kerberos的HBase集群环境下大规模使用的基础。后续我将开放Spark Cubing功能让感兴趣的用户使用。<strong>最后，十分感谢我们团队Spark 小伙伴的给力支持。</strong></p>

            <hr/>
            <div style="padding: 0; margin: 10px auto; width: 90%; text-align: center">
                <button id="rewardButton" , disable="enable" ,
                        onclick="var qr = document.getElementById('QR'); if (qr.style.display === 'none') {qr.style.display='block';} else {qr.style.display='none'}"
                        ,
                        style="cursor: pointer; border: 0; outline: 0; border-radius: 100%; padding: 0; margin: 0; letter-spacing: normal; text-transform: none; text-indent: 0px; text-shadow: none">
                    <span style="display: inline-block; width: 60px; height: 60px; border-radius: 100%; line-height: 58px; color: #fff; font-size:36px; font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, Helvetica, STKaiti, SimSun, serif; background: rgb(236,96,0)">赞</span>
                </button>
                <div id="QR" style="display: none;">
                    <p><img src="../images/weixin.jpeg" width="200" /></p>
                    <p><img src="../images/zhifubao.jpeg" width="200" /></p>
                </div>

            </div>
            <h3>评论</h3>
            <div id="vcomment"></div>
        </div>
        <div class="col-md-1"></div>
    </div>
</div>

<div class="row" style="padding-top: 60px">
    <div class="container center-block">
        <div class="col-md-1"></div>
        <div class="col-md-10 col-sm-12">
            <div class="ds-thread"
                 data-thread-key=595a09ffde02721206aa7bb2
                 data-title=Kylin 2.0 Spark Cubing 优化改进
                 data-url=kylin-spark-cubing>
            </div>
        </div>
        <div class="col-md-1"></div>
    </div>
</div>

<div class="footer">
    <a href="https://www.bcmeng.com/" target="_blank"  rel="nofollow">康凯森</a>
</div>

<script src="../js/code.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<script src="../js/jquery.min.js"></script>
<script src="../js/bootstrap.js"></script>
<script>
    var _hmt = _hmt || [];
    (function() {
        var hm = document.createElement("script");
        hm.src = "https://hm.baidu.com/hm.js?1d198a377ef466190881d1c021155925";
        var s = document.getElementsByTagName("script")[0];
        s.parentNode.insertBefore(hm, s);
    })();
</script>
<script src="../js/av-min.js"></script>
<script src='../js/Valine.min.js'></script>
<script type="text/javascript">
    window.valine = new Valine({
        el: '#vcomment' ,
        verify: true,
        notify: true,
        appId: 'BlLnB0re5OzQVzrgEplAxkyg-gzGzoHsz',
        appKey: 'wUyxSV0U4Vi7oK1EHK6ipErv',
        placeholder: '欢迎评论'
    });
</script>

</body>
</html>